﻿using System.Reflection;
using System.Text;
using Newtonsoft.Json;
using Nop.Core;
using Nop.Core.Infrastructure;

namespace Nop.Services.Plugins;

/// <summary>
/// Represents an information about plugins
/// </summary>
public partial class PluginsInfo : IPluginsInfo
{
    #region Fields

    protected const string OBSOLETE_FIELD = "Obsolete field, using only for compatibility";
    protected List<string> _installedPluginNames = new();
    protected IList<PluginDescriptorBaseInfo> _installedPlugins = new List<PluginDescriptorBaseInfo>();

    protected readonly INopFileProvider _fileProvider;

    #endregion

    #region Ctor

    public PluginsInfo(INopFileProvider fileProvider)
    {
        _fileProvider = fileProvider ?? CommonHelper.DefaultFileProvider;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Get system names of installed plugins from obsolete file
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the list of plugin system names
    /// </returns>
    protected virtual IList<string> GetObsoleteInstalledPluginNames()
    {
        //check whether file exists
        var filePath = _fileProvider.MapPath(NopPluginDefaults.InstalledPluginsFilePath);
        if (!_fileProvider.FileExists(filePath))
        {
            //if not, try to parse the file that was used in previous nopCommerce versions
            filePath = _fileProvider.MapPath(NopPluginDefaults.ObsoleteInstalledPluginsFilePath);
            if (!_fileProvider.FileExists(filePath))
                return new List<string>();

            //get plugin system names from the old txt file
            var pluginSystemNames = new List<string>();
            using (var reader = new StringReader(_fileProvider.ReadAllText(filePath, Encoding.UTF8)))
            {
                string pluginName;
                while ((pluginName = reader.ReadLine()) != null)
                    if (!string.IsNullOrWhiteSpace(pluginName))
                        pluginSystemNames.Add(pluginName.Trim());
            }

            //and delete the old one
            _fileProvider.DeleteFile(filePath);

            return pluginSystemNames;
        }

        var text = _fileProvider.ReadAllText(filePath, Encoding.UTF8);
        if (string.IsNullOrEmpty(text))
            return new List<string>();

        //delete the old file
        _fileProvider.DeleteFile(filePath);

        //get plugin system names from the JSON file
        return JsonConvert.DeserializeObject<IList<string>>(text);
    }

    /// <summary>
    /// Deserialize PluginInfo from json
    /// </summary>
    /// <param name="json">Json data of PluginInfo</param>
    /// <returns>True if data are loaded, otherwise False</returns>
    protected virtual void DeserializePluginInfo(string json)
    {
        if (string.IsNullOrEmpty(json))
            return;

        var pluginsInfo = JsonConvert.DeserializeObject<PluginsInfo>(json);

        if (pluginsInfo == null)
            return;

        InstalledPluginNames = pluginsInfo.InstalledPluginNames;
        InstalledPlugins = pluginsInfo.InstalledPlugins;
        PluginNamesToUninstall = pluginsInfo.PluginNamesToUninstall;
        PluginNamesToDelete = pluginsInfo.PluginNamesToDelete;
        PluginNamesToInstall = pluginsInfo.PluginNamesToInstall;
    }

    /// <summary>
    /// Check whether the directory is a plugin directory
    /// </summary>
    /// <param name="directoryName">Directory name</param>
    /// <returns>Result of check</returns>
    protected bool IsPluginDirectory(string directoryName)
    {
        if (string.IsNullOrEmpty(directoryName))
            return false;

        //get parent directory
        var parent = _fileProvider.GetParentDirectory(directoryName);
        if (string.IsNullOrEmpty(parent))
            return false;

        //directory is directly in plugins directory
        if (!_fileProvider.GetDirectoryNameOnly(parent).Equals(NopPluginDefaults.PathName, StringComparison.InvariantCultureIgnoreCase))
            return false;

        return true;
    }

    /// <summary>
    /// Get list of description files-plugin descriptors pairs
    /// </summary>
    /// <param name="directoryName">Plugin directory name</param>
    /// <returns>Original and parsed description files</returns>
    protected IList<(string DescriptionFile, PluginDescriptor PluginDescriptor)> GetDescriptionFilesAndDescriptors(string directoryName)
    {
        ArgumentException.ThrowIfNullOrEmpty(directoryName);

        var result = new List<(string DescriptionFile, PluginDescriptor PluginDescriptor)>();

        //try to find description files in the plugin directory
        var files = _fileProvider.GetFiles(directoryName, NopPluginDefaults.DescriptionFileName, false);

        //populate result list
        foreach (var descriptionFile in files)
        {
            //skip files that are not in the plugin directory
            if (!IsPluginDirectory(_fileProvider.GetDirectoryName(descriptionFile)))
                continue;

            //load plugin descriptor from the file
            var text = _fileProvider.ReadAllText(descriptionFile, Encoding.UTF8);
            var pluginDescriptor = PluginDescriptor.GetPluginDescriptorFromText(text);

            result.Add((descriptionFile, pluginDescriptor));
        }

        //sort list by display order. NOTE: Lowest DisplayOrder will be first i.e 0 , 1, 1, 1, 5, 10
        //it's required: https://www.nopcommerce.com/boards/topic/17455/load-plugins-based-on-their-displayorder-on-startup
        result = result.OrderBy(item => item.PluginDescriptor.DisplayOrder).ToList();

        return result;
    }

    #endregion

    #region Methods

    /// <summary>
    /// Get plugins info
    /// </summary>
    /// <returns>
    /// The true if data are loaded, otherwise False
    /// </returns>
    public virtual void LoadPluginInfo()
    {
        //check whether plugins info file exists
        var filePath = _fileProvider.MapPath(NopPluginDefaults.PluginsInfoFilePath);
        if (!_fileProvider.FileExists(filePath))
        {
            //file doesn't exist, so try to get only installed plugin names from the obsolete file
            _installedPluginNames.AddRange(GetObsoleteInstalledPluginNames());

            //and save info into a new file if need
            if (_installedPluginNames.Any())
                Save();
        }

        //try to get plugin info from the JSON file
        var text = _fileProvider.FileExists(filePath)
            ? _fileProvider.ReadAllText(filePath, Encoding.UTF8)
            : string.Empty;

        DeserializePluginInfo(text);

        var pluginDescriptors = new List<(PluginDescriptor pluginDescriptor, bool needToDeploy)>();
        var incompatiblePlugins = new Dictionary<string, PluginIncompatibleType>();

        //ensure plugins directory is created
        var pluginsDirectory = _fileProvider.MapPath(NopPluginDefaults.Path);
        _fileProvider.CreateDirectory(pluginsDirectory);

        //load plugin descriptors from the plugin directory
        foreach (var item in GetDescriptionFilesAndDescriptors(pluginsDirectory))
        {
            var descriptionFile = item.DescriptionFile;
            var pluginDescriptor = item.PluginDescriptor;

            //skip descriptor of plugin that is going to be deleted
            if (PluginNamesToDelete.Contains(pluginDescriptor.SystemName))
                continue;

            //ensure that plugin is compatible with the current version
            if (!pluginDescriptor.SupportedVersions.Contains(NopVersion.CURRENT_VERSION, StringComparer.InvariantCultureIgnoreCase))
            {
                incompatiblePlugins.Add(pluginDescriptor.SystemName, PluginIncompatibleType.NotCompatibleWithCurrentVersion);
                continue;
            }

            //some more validation
            if (string.IsNullOrEmpty(pluginDescriptor.SystemName?.Trim()))
                throw new Exception($"A plugin '{descriptionFile}' has no system name. Try assigning the plugin a unique name and recompiling.");

            if (pluginDescriptors.Any(p => p.pluginDescriptor.Equals(pluginDescriptor)))
                throw new Exception($"A plugin with '{pluginDescriptor.SystemName}' system name is already defined");

            //set 'Installed' property
            pluginDescriptor.Installed = InstalledPlugins.Select(pd => pd.SystemName)
                .Any(pluginName => pluginName.Equals(pluginDescriptor.SystemName, StringComparison.InvariantCultureIgnoreCase));

            try
            {
                //try to get plugin directory
                var pluginDirectory = _fileProvider.GetDirectoryName(descriptionFile);
                if (string.IsNullOrEmpty(pluginDirectory))
                    throw new Exception($"Directory cannot be resolved for '{_fileProvider.GetFileName(descriptionFile)}' description file");

                //get list of all library files in the plugin directory (not in the bin one)
                pluginDescriptor.PluginFiles = _fileProvider.GetFiles(pluginDirectory, "*.dll", false)
                    .Where(file => IsPluginDirectory(_fileProvider.GetDirectoryName(file)))
                    .ToList();

                //try to find a main plugin assembly file
                var mainPluginFile = pluginDescriptor.PluginFiles.FirstOrDefault(file =>
                {
                    var fileName = _fileProvider.GetFileName(file);
                    return fileName.Equals(pluginDescriptor.AssemblyFileName, StringComparison.InvariantCultureIgnoreCase);
                });

                //file with the specified name not found
                if (mainPluginFile == null)
                {
                    //so plugin is incompatible
                    incompatiblePlugins.Add(pluginDescriptor.SystemName, PluginIncompatibleType.MainAssemblyNotFound);
                    continue;
                }

                var pluginName = pluginDescriptor.SystemName;

                //if it's found, set it as original assembly file
                pluginDescriptor.OriginalAssemblyFile = mainPluginFile;

                //need to deploy if plugin is already installed
                var needToDeploy = InstalledPlugins.Select(pd => pd.SystemName).Contains(pluginName);

                //also, deploy if the plugin is only going to be installed now
                needToDeploy = needToDeploy || PluginNamesToInstall.Any(pluginInfo => pluginInfo.SystemName.Equals(pluginName));

                //finally, exclude from deploying the plugin that is going to be deleted
                needToDeploy = needToDeploy && !PluginNamesToDelete.Contains(pluginName);

                //mark plugin as successfully deployed
                pluginDescriptors.Add((pluginDescriptor, needToDeploy));
            }
            catch (ReflectionTypeLoadException exception)
            {
                //get all loader exceptions
                var error = exception.LoaderExceptions.Aggregate($"Plugin '{pluginDescriptor.FriendlyName}'. ",
                    (message, nextMessage) => $"{message}{nextMessage?.Message ?? string.Empty}{Environment.NewLine}");

                throw new Exception(error, exception);
            }
            catch (Exception exception)
            {
                //add a plugin name, this way we can easily identify a problematic plugin
                throw new Exception($"Plugin '{pluginDescriptor.FriendlyName}'. {exception.Message}", exception);
            }
        }

        IncompatiblePlugins = incompatiblePlugins;
        PluginDescriptors = pluginDescriptors;
    }


    /// <summary>
    /// Save plugins info to the file
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task SaveAsync()
    {
        //save the file
        var filePath = _fileProvider.MapPath(NopPluginDefaults.PluginsInfoFilePath);
        var text = JsonConvert.SerializeObject(this, Formatting.Indented);
        await _fileProvider.WriteAllTextAsync(filePath, text, Encoding.UTF8);
    }

    /// <summary>
    /// Save plugins info to the file
    /// </summary>
    public virtual void Save()
    {
        //save the file
        var filePath = _fileProvider.MapPath(NopPluginDefaults.PluginsInfoFilePath);
        var text = JsonConvert.SerializeObject(this, Formatting.Indented);
        _fileProvider.WriteAllText(filePath, text, Encoding.UTF8);
    }

    /// <summary>
    /// Create copy from another instance of IPluginsInfo interface
    /// </summary>
    /// <param name="pluginsInfo">Plugins info</param>
    public virtual void CopyFrom(IPluginsInfo pluginsInfo)
    {
        InstalledPlugins = pluginsInfo.InstalledPlugins?.ToList() ?? new List<PluginDescriptorBaseInfo>();
        PluginNamesToUninstall = pluginsInfo.PluginNamesToUninstall?.ToList() ?? new List<string>();
        PluginNamesToDelete = pluginsInfo.PluginNamesToDelete?.ToList() ?? new List<string>();
        PluginNamesToInstall = pluginsInfo.PluginNamesToInstall?.ToList() ??
                               new List<(string SystemName, Guid? CustomerGuid)>();
        AssemblyLoadedCollision = pluginsInfo.AssemblyLoadedCollision?.ToList();
        PluginDescriptors = pluginsInfo.PluginDescriptors;
        IncompatiblePlugins = pluginsInfo.IncompatiblePlugins?.ToDictionary(item => item.Key, item => item.Value);
    }

    #endregion

    #region Properties

    /// <summary>
    /// Gets or sets the list of all installed plugin names
    /// </summary>
    public virtual IList<string> InstalledPluginNames
    {
        get
        {
            if (_installedPlugins.Any())
                _installedPluginNames.Clear();

            return _installedPluginNames.Any() ? _installedPluginNames : [OBSOLETE_FIELD];
        }
        set
        {
            if (value?.Any() ?? false)
                _installedPluginNames = value.ToList();
        }
    }

    /// <summary>
    /// Gets or sets the list of all installed plugin
    /// </summary>
    public virtual IList<PluginDescriptorBaseInfo> InstalledPlugins
    {
        get
        {
            if ((_installedPlugins?.Any() ?? false) || !_installedPluginNames.Any())
                return _installedPlugins;

            if (PluginDescriptors?.Any() ?? false)
                _installedPlugins = PluginDescriptors
                    .Where(pd => _installedPluginNames.Any(pn =>
                        pn.Equals(pd.pluginDescriptor.SystemName, StringComparison.InvariantCultureIgnoreCase)))
                    .Select(pd => pd.pluginDescriptor as PluginDescriptorBaseInfo).ToList();
            else
                return _installedPluginNames
                    .Where(name => !name.Equals(OBSOLETE_FIELD, StringComparison.InvariantCultureIgnoreCase))
                    .Select(systemName => new PluginDescriptorBaseInfo { SystemName = systemName }).ToList();

            return _installedPlugins;
        }
        set => _installedPlugins = value;
    }

    /// <summary>
    /// Gets or sets the list of plugin names which will be uninstalled
    /// </summary>
    public virtual IList<string> PluginNamesToUninstall { get; set; } = new List<string>();

    /// <summary>
    /// Gets or sets the list of plugin names which will be deleted
    /// </summary>
    public virtual IList<string> PluginNamesToDelete { get; set; } = new List<string>();

    /// <summary>
    /// Gets or sets the list of plugin names which will be installed
    /// </summary>
    public virtual IList<(string SystemName, Guid? CustomerGuid)> PluginNamesToInstall { get; set; } =
        new List<(string SystemName, Guid? CustomerGuid)>();


    /// <summary>
    /// Gets or sets the list of plugin which are not compatible with the current version
    /// </summary>
    /// <remarks>
    /// Key - the system name of plugin.
    /// Value - the reason of incompatibility.
    /// </remarks>
    [JsonIgnore]
    public virtual IDictionary<string, PluginIncompatibleType> IncompatiblePlugins { get; set; }

    /// <summary>
    /// Gets or sets the list of assembly loaded collisions
    /// </summary>
    [JsonIgnore]
    public virtual IList<PluginLoadedAssemblyInfo> AssemblyLoadedCollision { get; set; }

    /// <summary>
    /// Gets or sets a collection of plugin descriptors of all deployed plugins
    /// </summary>
    [JsonIgnore]
    public virtual IList<(PluginDescriptor pluginDescriptor, bool needToDeploy)> PluginDescriptors { get; set; }

    #endregion
}